Repository navigation
Answer the issue, merge request, notification, push mirror and project rows of issue 1240 from GitLab's source - #1570
Merged
jmrplens merged 8 commits intoOct 28, 2026
Conversation
…swer the assignee row Issue 1240 lists the assignee_ids row of doc/api/issues.md, which marks the parameter of issue.create Premium and Ultimate only while every tier's schema offers it. Read against GitLab's source at v19.5.0-ee, the parameter is not what the tier gates: every tier takes it, and what needs Premium is a second assignee. IssuableBaseService#filter_assignees_using_checks keeps only the first ID unless the issue allows multiple assignees (app/services/issuable_base_service.rb:117-122), which Community Edition never does and Enterprise Edition does where multiple_issue_assignees is licensed (ee/app/models/ee/issue.rb:195-197), a feature every Premium license carries. On GitLab.com, whose license carries every feature, the namespace's plan decides (ee/app/models/ee/project.rb:1876-1886), so a Free namespace keeps the first as an unlicensed instance does. A tag would take the parameter from a Free caller who assigns one user, so the descriptions of issue.create and issue.update, which goes through the same filter, say that more than one assignee needs Premium or Ultimate and that below them, on an unlicensed instance or a Free GitLab.com namespace, GitLab keeps the first. The declaration table of cmd/audit_edition_tier answers the row as a value-level gate with the source that settles it. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the issue page's row, which stays an open finding on the parent. The new CE scenario, TestIssueCreate_TwoAssignees_OnTheFreeTier_KeepsOnlyTheFirst, creates an issue for two project members on every surface of an unlicensed instance and holds the answer to the first of them, which is what the description now promises. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
Issue 1240 lists the approver_ids rows of doc/api/merge_requests.md, which mark the filter of merge_request.list, list_global and list_group Premium and Ultimate only while every tier's schema offered it. Read at v19.5.0-ee, Enterprise Edition declares the filter (ee/lib/ee/api/merge_requests.rb:18-21) and applies it on every tier, with no license check (EE::MergeRequestsFinder#by_approvers, ee/app/finders/ee/merge_requests_finder.rb:16-20), but what it matches is approval rules: the users of a merge request's or a project's rules, direct or through a group (MergeRequests::ByApproversFinder, ee/app/finders/merge_requests/by_approvers_finder.rb:71-105). A project gets rules through the approval rule routes, which refuse an instance without the Premium merge_request_approvers feature (ee/lib/api/helpers/project_approval_rules_helpers.rb:68-70), so below Premium the filter finds only the rules left from a licensed time, and None answers every merge request. Community Edition does not declare it. That is the shape of the group wiki filters of the layer below, so the filter takes the Premium tag on the three list inputs, and the tier filter prunes it from the Free schemas. The approved-by filters stay on every tier, since approving a merge request is a Free feature. TestCentralTierFilter_MergeRequestApproverFilter_IsOfferedFromPremium holds the three filters to Premium and the approved-by ones to Free, and fails on the three on the parent; TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the page's three rows, all open on the parent. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…s it, from Premium Issue 1240 lists the new_epic rows of doc/api/notification_settings.md, which mark the flag of the account-wide, group and project notification updates Premium and Ultimate only while every tier's schema offered it. The page documents the group and the project update in one table, and GitLab's source at v19.5.0-ee answers the two scopes differently. A project's update never takes the flag. Enterprise Edition declares new_epic for the account-wide settings and for a group's, and not for a project's (EE::NotificationSetting::EMAIL_EVENTS_MAPPING, ee/app/models/ee/notification_setting.rb:7-17), the routes declare exactly the events that mapping returns (lib/api/notification_settings.rb:41 and 108), and the live record agrees, so Grape dropped the flag from a project's update on every tier while the schema offered it. The project input no longer carries it. The account-wide and group updates store it on every tier, but an epic exists only from Premium, so the flag follows the Premium gate the epic actions already have, as the group wiki settings of the layer below do. It moves out of the events the three inputs share into an epicEvent that only those two embed, tagged Premium, and their handlers copy it onto the options the shared builder returns. The struct audit's declaration table records why the shared events leave out a field client-go's options carry. TestCentralTierFilter_EpicNotification_IsOfferedWhereGitLabTakesIt holds the flag to Premium on the account-wide and group updates and to no tier on a project's, beside two shared events that stay on Free; on the parent it fails on all three. TestUpdateSettingsForGroup_NewEpic_SendsTheFlagAlone holds the group handler to sending the flag it no longer gets from the builder, and the per-event table now sets each event on the account-wide input, the one that carries all eighteen. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the page's three rows, all open on the parent. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
… not gated Issue 1240 lists the mirror_branch_regex rows of doc/api/remote_mirrors.md, which mark the branch filter of project.mirror_add and project.mirror_edit Premium and Ultimate only while every tier's schema offers it. GitLab's source at v19.5.0-ee does not gate it. Enterprise Edition declares the parameter on both routes with no license check (ee/lib/ee/api/helpers/remote_mirrors_helpers.rb:11-14), lets the services save it (ee/app/models/ee/remote_mirrors/attributes.rb:8-24) and pushes only the branches it matches on every tier (EE::RemoteMirror#options_for_update and #branches_to_sync, ee/app/models/ee/remote_mirror.rb:22-35). Community Edition does not declare it and drops it, as it drops every Enterprise Edition parameter, which the tier model, where Free covers both editions, cannot tell apart. The schema stays as it is, and the declaration table answers both rows as not gated, with the source that settles them; the rows are a documentation fix to offer GitLab. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the page's two rows, both open on the parent. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
… GitLab honors them at Issue 1240 lists 26 rows of doc/api/projects.md that mark a parameter of project.create, project.create_for_user, project.list or project.update for a paid tier while every tier's schema offered it. Read against GitLab's Grape params and services at v19.5.0-ee, 22 of them, and two update parameters the page does not mark, are dropped or refused below their tier, so a schema offering them there invited a call GitLab answers with success and without the setting. They take a tier tag: - external_authorization_classification_label on both creates and the update, allow_pipeline_trigger_approve_deployment and ci_restrict_pipeline_cancellation_role on the update, Premium, and prevent_merge_without_jira_issue on the update, Ultimate, which EE::API::Helpers::ProjectsHelpers#filter_attributes_using_license! deletes unless the instance's license carries external_authorization_service_api_management, protected_environments, ci_pipeline_cancellation_restrictions or jira_issue_association_enforcement (ee/lib/ee/api/helpers/projects_helpers.rb:124-142); - use_custom_template, template_project_id and group_with_project_templates_id on both creates, Premium: a custom template is used only where the instance's license carries custom_project_templates (EE::Projects::CreateFromTemplateService#use_custom_template?, ee/app/services/ee/projects/create_from_template_service.rb:60-64, and ee/app/models/ee/application_setting.rb:908-910), and without it a template_project_id falls to the built-in templates and is refused; - issues_template and merge_requests_template on the update, Premium, deleted unless the project carries issuable_default_templates (ee/lib/ee/api/projects.rb:132-137); - mirror and mirror_trigger_builds on both creates, and on the update those two, mirror_user_id, only_mirror_protected_branches, mirror_overwrites_diverged_branches and, unmarked by the page, mirror_branch_regex and import_url, Premium: each is a pull mirror attribute, which the create applies and the update keeps only for a caller with admin_mirror (ee/app/services/ee/projects/create_service.rb:107-115 and ee/lib/ee/api/projects.rb:124-130), and admin_mirror needs the repository_mirrors feature (ee/app/policies/ee/project_policy.rb:38 and 757). Community Edition does not declare the update's import_url, so it is the mirror's URL and nothing else there. repository_mirrors is also a registration feature of a Free self-managed instance with Service Ping on; the settings take its license tier, as the group wiki and group webhook ones already do here. Two rows stay on every tier's schema, answered in the declaration table of cmd/audit_edition_tier with the source that settles them: - include_hidden on project.list is not gated: Enterprise Edition keeps it for an administrator on every tier and its finder applies it (ee/lib/ee/api/projects.rb:116-121 and ee/app/finders/ee/projects_finder.rb:49-51). Its description now says it is for administrators. - auto_duo_code_review_enabled on the update is a gate on something other than a value. GitLab keeps it where Duo code review is available to the project: the instance's license must carry review_merge_request, a Premium feature rather than the Ultimate the row says, and the project must have the Duo Agent Platform's code review flow or a Duo Enterprise add-on, which on GitLab.com a Free group with GitLab Credits reaches. A tag would take it from that group, so its description says where it applies. The issue's last two project rows, the checksum filters of project.list, are the next commit's. project.create_for_user embeds the create input, so its template_project_id, which the page does not list for that route, takes the same tag. TestCentralTierFilter_ProjectSettings_AreOfferedFromTheTierGitLabHonorsThem holds 29 parameters to the tier their schema is offered from on the three catalogs, the three that stay on Free among them, and fails on 26 assertions on the parent. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the page's 24 rows of these four routes, which stay open findings on the parent. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…where Issue 1240 lists the repository_checksum_failed and wiki_checksum_failed rows of doc/api/projects.md, which mark both project.list filters Premium and Ultimate only while every tier's schema offered them. Neither a tag nor a description answers them, because no tier honors them. Enterprise Edition declares both on every route that takes the project list filters (optional_filter_params_ee, ee/lib/ee/api/helpers/projects_helpers.rb:38-42 at v19.5.0-ee), and nothing reads them: the finder parameters the routes build carry include_hidden and with_security_reports and neither of these (EE::API::Helpers#project_finder_params_ee, ee/lib/ee/api/helpers.rb:119-127), and no finder, model or service in the tree names them. A list asked for the projects whose checksum failed answered every project, on every tier. The five project lists that carried them, project.list, list_forks, list_user_projects, list_user_contributed and list_user_starred, no longer offer either filter, and the option builder they share no longer copies them. A user's contributed projects never took them: that route declares order_by, sort, simple, user_id and the pagination alone (lib/api/projects.rb:380-387), so Grape dropped both there, and the comment on the shared filter input no longer says the three user-scoped routes take the full filter set. The struct audit's declaration table records why userProjectFilter leaves out two fields client-go's ListProjectsOptions carries, as it does for the deployment merge request options the route declares and never reads. TestCentralTierFilter_ProjectChecksumFilters_AreOfferedOnNoTier holds both filters out of the five lists on the three catalogs and fails on all 30 assertions on the parent. The option builder tests now hold that a list built with every filter a caller can set sends neither, and TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the two rows, open on the parent, so that no finding of the project page stays open. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…b's source does not gate Answering issue 1240 read 65 rows of GitLab's API pages that mark a request parameter for a paid tier against the source at v19.5.0-ee. Most of them the source confirms, and the schemas took a tier tag or a description. The rest are a page that disagrees with the code, and the upstream register gains row 142 for them, in two groups. In the first the source settles the row and the page is what is wrong: the group list's marked_for_deletion_on, which Community Edition applies; the group variable filter, read on every tier and edition; new_epic in the table the notification page shares between a group's update and a project's, which a project's does not declare; the two project list checksum filters, declared and read by nothing; and the automatic Duo code review, marked Ultimate where a Premium license with Duo code review keeps it. In the second Enterprise Edition applies the parameter on every tier with no license check while the feature page sells it as Premium: the group membership lock and compute minute limits, the push mirror branch regex and the hidden project filter. Those are a question for GitLab before they are a merge request, since either the page or the missing license check can be the defect. The contributor page of cmd/audit_edition_tier says that a parameter no tier honors is answered by removing it from the input, with the struct audit declaration that says why, and points the documentation fixes at the new row. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
The issue layer adds test/e2e/gitlab/ce/issues_test.go, the scenario that holds an issue created for two assignees on an unlicensed instance to the first of them, and the e2e file counts in CLAUDE.md and the repository map still said the ce package holds six. It holds seven. Refs #1240 Signed-off-by: José M. Requena Plens <mail@jmrp.io>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This was referenced Oct 30, 2026
Merged
jmrplens
added a commit
that referenced
this pull request
Oct 30, 2026
Every route that takes the project list filters declares repository_checksum_failed and wiki_checksum_failed among optional_filter_params_ee, and nothing reads them: the finder params the routes build carry neither, and no finder, model or service names them. The answer to the project rows of issue 1240 (#1570) stopped offering them on the project lists for that reason, while ListProjectsOptions still models both, so on main R-PARAM reports each as not_offered on GET /projects, the forks, a user's projects and a user's starred projects: eight findings no declaration answers, where this layer leaves none. Each is declared declared-not-applied, with the reason the structs scope already records for leaving them out, and TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads holds the eight to the declaration; its eight subtests fail without it. The scope reports not_offered 0 again and 1491 declared (1510 params), and request-paths.md says so. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens
added a commit
that referenced
this pull request
Oct 30, 2026
Every route that takes the project list filters declares repository_checksum_failed and wiki_checksum_failed among optional_filter_params_ee, and nothing reads them: the finder params the routes build carry neither, and no finder, model or service names them. The answer to the project rows of issue 1240 (#1570) stopped offering them on the project lists for that reason, while ListProjectsOptions still models both, so on main R-PARAM reports each as not_offered on GET /projects, the forks, a user's projects and a user's starred projects: eight findings no declaration answers, where this layer leaves none. Each is declared declared-not-applied, with the reason the structs scope already records for leaving them out, and TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads holds the eight to the declaration; its eight subtests fail without it. The scope reports not_offered 0 again and 1491 declared (1510 params), and request-paths.md says so. Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens
added a commit
that referenced
this pull request
Oct 30, 2026
… here sends (R-PARAM) (#1639) Refs #1595 Refs #1596 Refs #1598 Refs #1597 Refs #1599 This is the fifth layer of the auditor stack. A param GitLab declares on a route we already call, and that client-go's option struct does not model, was seen by no dimension: client-go cannot send it, so no input offers it, so no recorded request carries it. The params of row 133 of the upstream register came from reading the diff between two records by hand. ## What the check detects The first commit moves the option struct reader out of R-PATH into `cmd/audit_1to1/internal/sdkoptions`, so the always-sent check and the new scope read one answer. A field is read with the encoder its request uses: `json` for a `NewRequest` PATCH, POST or PUT, `url` for every other verb and for every field of an `UploadRequest`, whose constructor `cmd/internal/sdkroutes` now records per method. The walk goes into any client-go struct a field carries (`[]*HookCustomHeader` is `custom_headers[key]`), and a type defined on an option struct (`CreateProjectForUserOptions`) is read as that struct. The always-sent check used to judge DELETE by the `json` tag and left GET unjudged; it now walks 614 endpoints instead of 289 and still reports the same 11 rows. The second commit adds `-scope=params` (`cmd/audit_1to1/internal/params`). Per route, over the union of the actions reaching it, a param is covered when a recorded request carries it or a request sends it on every call. The inventory records the top-level keys of a body, so a recorded key covers a param nested under it only where an option struct of the route models that param. Otherwise it is `unmodeled` (nothing here can send it), `not_offered` (the struct the action already hands client-go models it and no input offers it), `offered_unrecorded` (an input offers it and no recorded request carries it) or `unjudged` (the options cannot be read). A raw request is read through the static type of its options, which the sdk scope's walk now exports (`sdk.RawRequests`), and the redirect GitLab answers another route with carries no options of its own. An input offers a param by name, through the requiredness scope's aliases, through its nested schema, or flattened with `shared.NormalizeSDKTag` (`not[author_id]` as `not_author_id`). Path params and the attributes Workhorse fills are not judged. The findings are a report. The gate fails only on a declaration that answers nothing, names no category or reason, or names a register row that does not exist; an `upstream` declaration answers an `unmodeled` param alone, so it goes stale the day client-go models it. The scope reads the committed request record, so CI runs it deferred with R-GRANT (`make audit-1to1-params`) and it is step 30 of `make analyze`. ## Witness test, red then green `TestRun_Repository_ReportsTheParamsClientGoV3300CannotSend` pins what the scope sees on v3.30.0: `source_project_id` and the twelve other new filters on the four merge request lists, the deployment one included; the system hooks' `url_variables` and `custom_headers`; `url_variables` on the project and group hooks; the three `duo_*` flags of `PUT /projects/:id`; `size` on a registry repository; `not[author_id]` on the three `issues_statistics` routes and the group epics; `include_html_description` on a release read, which `GetRelease` takes no options to send; `approval_rules[id]` of a protected environment, nested under the `approval_rules` the inventory records and modeled by no field of `EnvironmentApprovalRuleOptions`; the declared `external`; the four `duo_*` flags GitLab drops on both create routes; and the four label collection routes as `branch-not-taken`. It also holds that a project hook add's custom header keys are covered only through `AddProjectHookOptions`, the struct its raw request sends, and that the edit's stand under the `custom_headers` container `EditProjectHookOptions` models. The bump stack changes it, which is the proof the scope ran again. A copy that reads the report as plain JSON fails on the parent (47f2535a0), where no scope reports the param (`paths` names `source_project_id` only as a response key, under `shapes`): ```text $ go test -count=1 -run TestZZWitness -v ./cmd/audit_1to1/ zz_witness_test.go:22: the params scope did not run: invalid scope "params" (valid: structs, actions, metadata, enums, sdk, paths, grants, required, all); nothing reports a param client-go cannot send --- FAIL: TestZZWitness_SourceProjectIDOnTheFourMergeRequestLists (0.00s) ``` and passes here: ```text zz_witness_test.go:48: GET /merge_requests source_project_id unmodeled zz_witness_test.go:48: GET /projects/:id/merge_requests source_project_id unmodeled zz_witness_test.go:48: GET /groups/:id/merge_requests source_project_id unmodeled zz_witness_test.go:48: GET /projects/:id/deployments/:deployment_id/merge_requests source_project_id unmodeled --- PASS: TestZZWitness_SourceProjectIDOnTheFourMergeRequestLists (3.68s) ``` The stand-in tests hold both client-go states: a list option without `merged_after` gives the finding and its `upstream` declaration answers it; with the field it turns `not_offered`, the declaration goes stale and the gate fails. They also hold that a member nested under a recorded key that the stand-in's struct does not model is `unmodeled`, and that a redirect another action follows leaves its route's params judged. ## What it finds on main, and how each is answered On the 19.5.0 record it judges 5791 params of 1101 reached routes and covers 2792. The summary counts findings, and a container that stands for its members is one finding: 785 `unmodeled` on 168 routes (905 params), 79 `not_offered` on 13 (85), 523 `offered_unrecorded` on 98 (537), 3 `unjudged`, and 1445 declared (1458). Declared: the settings update and every integration take a map (`map-input`, 1371), rows 59 and 133 of the register (`upstream`, 42), four `duo_*` flags GitLab drops on create (`declared-not-applied`, 8, from `filter_duo_vulnerability_attributes!`), the GitHub payload of the mirror pull (`webhook-payload`, 8), and every param of the four label collection routes, which client-go takes only for a nil label argument and our handlers never pass one (`branch-not-taken`, 16). The 79 `not_offered` are what the next layer answers (#1596): 61 `override_params[...]` of the file import, three list flags of the deployment merge requests, the avatar on eight create and edit actions, and `repository_checksum_failed` and `wiki_checksum_failed` on the four project lists, which main stopped offering in #1570 because GitLab reads neither. The `unmodeled` backlog goes to client-go (#1598), and gating the undeclared findings waits on it (#1599). One finding is a defect of ours, and I left it undeclared so it stays visible: `admin.terraform_state_lock` sends none of the seven lock fields GitLab requires, because client-go's `TerraformStates.Lock` takes no options, so every call is refused with `ID is missing` (#1597). No category for a protocol body is added: the one candidate is that defect, left visible. The figures `request-paths.md` and `cmd-utilities.md` give in this layer are the ones I measured before the stack was rebased onto main (613 endpoints for the always-sent check, and for R-PARAM 2794 covered, 71 `not_offered` on 9 routes and 534 `offered_unrecorded` on 99); main's newer request record and #1570 have moved them since. The top of the stack writes there what the merged tree measures. ## Checks `go test ./... -count=1` with the snapshot parity deferred, and every `cmd/audit_1to1` suite on the real tree without it. 100% statements and every condition both ways (gobco) on `sdkoptions`, `params`, `sdkroutes`, `paths`, `required`, `sdk` and the command. gremlins: Lived 0 and Not covered 0 on each, except `paths` (536 killed), whose 490 not covered are package-level initializers in seven files of `paths` this layer does not touch (`endpoint_declarations.go`, `endpoints.go`, `orbit_declarations.go`, `pagination_declarations.go`, `published.go`, `sent_declarations.go` and `shape_declarations.go`), which the coverage profile does not mark. The touched `make analyze` steps pass, and `check-action-grants-derivation` is unchanged. --------- Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens
added a commit
that referenced
this pull request
Oct 30, 2026
…ady models, and declare the ones GitLab drops (R-PARAM) (#1640) Closes #1596 Refs #1598 Refs #1595 This layer acts on what R-PARAM found that this repository can fix today. On its parent the scope lists 79 `not_offered` findings on 13 routes: params the option struct an action already hands client-go models, and that no input offers. I answered every one, offering what GitLab applies and declaring what GitLab drops or cannot apply. A witness test pins 71 of them to that answer, and a second one the eight checksum filters of the project lists, which main stopped offering in #1570. ## What the check detects `TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300` reads the R-PARAM report of the real tree and pins 71 findings of v3.30.0: the avatars, the 61 `override_params` keys and the three list flags. Each param that is offered now has to be offered and answered by no declaration, and each one that is declared has to be `declared-not-applied` on a param client-go models and no input offers, `lfs_enabled` among them, which the import input offered before this stack. It is a witness and not a gate on the class: R-PARAM stays a report with its gate on stale declarations, as decided, so a param client-go starts modeling in the bump is reported as `not_offered` beside the rest and the bump updates this list the way it updates every witness. The layer also stops a `declared-not-applied` or `webhook-payload` declaration from answering a param an input offers. Before, it answered a finding of any class, so it could hide the very defect this layer fixes: had `lfs_enabled` come back into the import input, the declaration would have answered the `offered_unrecorded` finding in silence. Now the declaration goes stale and the gate fails, which I checked by putting the field back: `POST /projects/import override_params[lfs_enabled] (declared-not-applied): answers no finding`. ## Witness test, red then green On the parent (2c79544e2, with this test file and nothing else of the layer) 72 subtests of the witness fail, the 46 params that are offered here and the 26 that are declared, and so do the eight of `TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads`, one per list and filter: ```text $ go test -count=1 -run 'TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300|TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads' ./cmd/audit_1to1/internal/params/ analyze_test.go:622: POST /groups avatar = {Route:POST /groups Param:avatar Class:not_offered SDKModels:true Offered:false ...}, want it offered and answered by no declaration analyze_test.go:629: POST /projects/import override_params[topics] = {... Class:not_offered SDKModels:true Offered:false ...}, want the modeled param declared declared-not-applied and offered by no input analyze_test.go:629: POST /projects/import override_params[lfs_enabled] = {... Class:offered_unrecorded SDKModels:true Offered:true ...}, want the modeled param declared declared-not-applied and offered by no input analyze_test.go:656: GET /projects repository_checksum_failed = {Route:GET /projects Param:repository_checksum_failed Class:not_offered SDKModels:true Offered:false ...}, want the modeled param declared declared-not-applied and offered by no input FAIL github.com/jmrplens/gitlab-mcp-server/v3/cmd/audit_1to1/internal/params ``` and both pass here: ```text --- PASS: TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300 --- PASS: TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads ok github.com/jmrplens/gitlab-mcp-server/v3/cmd/audit_1to1/internal/params ``` The class restriction has its own unit cases, red before it (`not applied on an offered param` and `webhook payload on an offered param`, `answers = true, want false`) and green after. ## What it found on main, and how each is answered The avatar on seven create and edit routes: `group.create`, `project.create`, `project.create_for_user`, `user.create`, `user.modify`, `admin.topic_create` and `admin.topic_update` now take an `avatar` object (the file name GitLab reads the image type from, and a local file or base64 content), refused before any request when it cannot be read. With it client-go sends the call as a multipart upload. `interactive.project_create` reaches the same route, which `project.create` now covers. That upload writes only the first value of each field (`UploadRequest`, `fields.Get`). So beside an avatar the project create sends its topics and tags, and the group create its download limit allowlist, as one comma-separated value, which GitLab splits back (`CommaSeparatedToArray`). The group alert list is an `Array[Integer]` GitLab does not split, so a create that gives one beside an avatar is refused and pointed at `group.update`. The test that holds the allowlist was red on the commit before the fix: the mock read `["alice"]` for `alice` and `bob`. The defect is now row 151 of the upstream register, with the three sites. The upstream wait table cannot hold it, because its predicates read types and routes and this is the body of a function, so `TestSDK_CreateProjectWithAvatar_WritesTheFirstTopicAlone` pins what client-go sends and fails the day it sends more. The 61 `override_params` keys of `project.import_from_file`: I read the import in GitLab's source. The overrides reach the project only through the tree restorer, which keeps the attributes `lib/gitlab/import_export/project/import_export.yml` permits for a project (`AttributesPermitter`, through `Group::RelationTreeRestorer#filter_attributes`), and then hands them to `Project#assign_attributes`. Of the 86 keys GitLab declares there it permits 52, and two of those it cannot assign: `topics` is the project's association with `Projects::Topic`, which a list of names does not fill, and the project has no `jobs_enabled` writer at all, since `lib/api/projects.rb` renames it to `builds_enabled` on the create and the edit and never on the import. Either one raises inside the restore, which fails the import. That is a trace of GitLab's code, which I did not run against an instance; no e2e scenario imports with overrides. The input now offers the 50 keys the import applies, 39 of the 61 among them and 11 it offered before. The other 22 of the 61 are declared `declared-not-applied`, and so are `lfs_enabled` and the 13 dropped keys client-go does not model, 36 in all, so none of them waits on a client-go change that would not make GitLab apply it. The input also stopped offering five keys it offered before this stack: `name`, `path`, `namespace_id` and `default_branch`, which Grape drops because the import does not declare them under `override_params`, and `lfs_enabled`, which the restorer excludes by name. The three list flags of a deployment's merge requests (`view`, `with_labels_details`, `with_merge_status_recheck`) are declared `declared-not-applied`: `lib/api/deployments.rb` presents `MergeRequestBasic` with no serializer options, so the route applies none of them. Two things came out of the work. `PUT /groups/:id`, `PUT /projects/:id` and `PUT /topics/:id` remove the avatar when it is sent blank, which client-go writes for an empty avatar, and no action did it: `group.update`, `project.update` and `admin.topic_update` take `remove_avatar`. And the request inventory now records the part names of a multipart form, so an upload's options can cover a param the way a JSON body's keys do. ## The report after On the committed request record the scope reports `not_offered` 0 (was 79), `unmodeled` 773 (was 785) and 1491 declared (was 1445). Eight of the declarations answer `repository_checksum_failed` and `wiki_checksum_failed` on the four project lists, which main stopped offering in #1570 because GitLab reads neither, and which R-PARAM would otherwise report `not_offered`; `TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads` holds them. Of the 13 dropped import keys client-go does not model, 12 left `unmodeled` for their declaration; the thirteenth, `duo_code_review_decisions_enabled`, was already answered by row 133, whose text now says the import would not apply it. The params offered here show as `offered_unrecorded` (568 on 103 routes, was 523 on 98) until the inventory is regenerated, which the stack does once at its top. There the recorded requests carry every param this layer offers: covered goes from 2792 to 2864, `offered_unrecorded` falls to 496 on 92 routes, and no avatar or `override_params` key the layer offers is left among the findings. The R-PARAM figures `request-paths.md` gives in this layer (2794 covered, 579 `offered_unrecorded` on 104) are the ones I measured before the stack was rebased onto main, and the top writes there what the merged tree measures. The `unmodeled` backlog, grouped by client-go service, is #1598. ## Checks `go test ./... -count=1` with the snapshot parity deferred, and every `cmd/audit_1to1` suite on the real tree without it. 100% statements and every condition both ways (gobco) on the nine touched packages. gremlins on the files the layer changes, the rest of each package excluded: Lived 0 and Not covered 0 in `topics`, `users`, `groups`, `projects`, `projectimportexport`, `params` and `testutil`. The first run left one mutant alive in the multipart recorder (`||` to `&&` on the boundary check), and its test now holds that a form without a boundary is refused before its body is read; `packageOutside`, a tagless switch from #598 whose case expressions gremlins could not see, is a chain of ifs now, which took `testutil` from 3 not covered to none. The `structs` and `adminspecs` changes are table entries and a string. `golangci-lint` is clean on them, and the `make analyze` steps the layer touches pass, as do markdownlint and the doc link check on the four contributor pages it edits. The new end-to-end scenarios compile and `make check-e2e-static` passes; I did not run them against a GitLab here. `gen_testing_docs --check` reports the test counts stale, as it already did below this layer, and is regenerated at the top. --------- Signed-off-by: José M. Requena Plens <mail@jmrp.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #1240 (third of three layers).
Issue 1240 lists 65 request parameters that GitLab's API pages mark for a paid tier while a lower tier's schema offers them. The two layers below built the declaration table in
cmd/audit_edition_tierand answered the 30 group, member, LDAP link and group variable rows. This layer reads the last 35, on the issue, merge request, notification settings, push mirror and project pages, against GitLab's Grape params and services at v19.5.0-ee, and gives each one an answer: a tier tag, a description, a declaration with the reason, or, where no tier honors the parameter at all, its removal from the input.What changes
Twenty-seven of the rows take a tier tag, so the tier filter prunes them from the lower tiers' schemas, because GitLab drops or refuses them below their tier:
approver_idsonmerge_request.list,list_globalandlist_group, Premium. Enterprise Edition applies the filter on every tier, but it matches approval rules, which a project gets only through routes refused withoutmerge_request_approvers, so below Premium it finds the rules left from a licensed time andNoneanswers every merge request.new_epicon the account-wide and group notification updates, Premium: stored on every tier, but an epic exists only from Premium.external_authorization_classification_label,use_custom_template,template_project_idandgroup_with_project_templates_id, Premium; on the update, the classification label,allow_pipeline_trigger_approve_deployment,ci_restrict_pipeline_cancellation_role,issues_templateandmerge_requests_template, Premium, andprevent_merge_without_jira_issue, Ultimate. The classification label, the deployment approval, the cancellation role and the Jira requirement are deleted byfilter_attributes_using_license!unless the license carries their feature, and the two description templates byverify_issuable_default_templates_attrs!unless the project carriesissuable_default_templates; a custom template is used only where the license carriescustom_project_templates, and without it atemplate_project_idis refused.mirrorandmirror_trigger_buildson both creates, and on the update those two,mirror_user_id,only_mirror_protected_branches,mirror_overwrites_diverged_branchesand, unmarked by the page,mirror_branch_regexandimport_url. Each needsadmin_mirror, which needs therepository_mirrorsfeature: on a create,EE::Projects::CreateService#setup_pull_mirroringappliesmirrorandmirror_trigger_buildsonly for a caller who has it, and on the updateverify_mirror_attrs!deletes every pull mirror attribute from a caller who has not.repository_mirrorsis also a registration feature, so a Free self-managed Enterprise Edition instance with Service Ping and registration features turned on honors every one of these. The tags follow its license tier all the same, as the pull mirror actions beside them (project.pull_mirror_get,pull_mirror_configureandstart_mirroring), the group wiki and the group webhooks already do here.auto_duo_code_review_enabledbelow is answered the other way because it follows the group Duo settings, which the layer below kept on every tier with a description, since a Free group with GitLab Credits keeps them on GitLab.com.Three parameters stop being offered on any tier, because GitLab honors them on none:
new_epicon a project's notification update. The page documents the group and project updates in one table, and only the group's declares the flag (EE::NotificationSetting::EMAIL_EVENTS_MAPPING), so Grape dropped it from every project update. The flag moved out of the events the three inputs share into anepicEventonly the account-wide and group inputs embed.repository_checksum_failedandwiki_checksum_failedon the five project lists. Four of their routes declare both and nothing reads them, so a list asked for the failed checksums answered every project; a user's contributed projects declare neither, nor any list filter butorder_by,sortandsimple, so Grape dropped both there.The struct audit's
acceptedMissingInputsrecords why those inputs leave out fields client-go's options carry.Two rows keep the parameter on every tier with a description that says which part needs the tier:
assignee_idsonissue.create: every tier takes it, and below Premium, on an unlicensed instance or a Free GitLab.com namespace, GitLab keeps only the first assignee (IssuableBaseService#filter_assignees_using_checks; on GitLab.com the namespace's plan decides).issue.updategoes through the same filter and says the same.auto_duo_code_review_enabledonproject.update: kept where Duo code review is available to the project, which needs a Premium license (not the Ultimate the row says) with GitLab Duo, or, on GitLab.com, a Free group with GitLab Credits, so a tag would take it from that group.Three rows are not gated, and the declaration table records the source line that says so:
mirror_branch_regexon the push mirror create and update, andinclude_hiddenonproject.list, which Enterprise Edition applies on every tier, the latter for an administrator. Community Edition drops both, as it drops every Enterprise Edition parameter, which the tier model, where Free covers both editions, cannot tell apart.The upstream register gains row 142 for the rows where GitLab's page disagrees with its code, across the whole issue, and the contributor page of the command says how a parameter no tier honors is answered. A documentation merge request for the rows the source settles is drafted and not sent; the rows where Enterprise Edition applies a parameter its page sells as Premium are a question for GitLab before they are a merge request.
How I tested it
The tests came first, and each failed on the parent.
TestCentralTierFilter_MergeRequestApproverFilter_IsOfferedFromPremium,TestCentralTierFilter_EpicNotification_IsOfferedWhereGitLabTakesIt,TestCentralTierFilter_ProjectSettings_AreOfferedFromTheTierGitLabHonorsThemandTestCentralTierFilter_ProjectChecksumFilters_AreOfferedOnNoTierbuild the Free, Premium and Ultimate catalogs and hold 49 parameters to the tiers their schema is offered at, those that must stay on Free and those offered on no tier among them; on the parent 3, 3, 25 and 10 of their cases failed.TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettlesgains the rows of the five pages and holds that none keeps an open finding and that each declaration answers its row with its category; every one of the 35 rows was open on the parent.TestUpdateSettingsForGroup_NewEpic_SendsTheFlagAloneholds the group handler to sending the flag the shared builder no longer copies, and the project option builder tests hold that a list built with every filter sends neither checksum filter.A new CE scenario,
TestIssueCreate_TwoAssignees_OnTheFreeTier_KeepsOnlyTheFirst, creates an issue for two project members on every surface of an unlicensed instance and holds the answer to the first, which is what the description now promises. It compiles here and runs with the CE suite at the top of the stack, and CLAUDE.md and the repository map now count it among the seven files of the CE package. The other rows rest on what a Free Enterprise Edition instance does, which no Docker instance of the suite is, so I read them from the source.Measured offline over the API pages read on 2026-10-27,
go run ./cmd/audit_edition_tier/ -offlinereports no open parameter finding and 28 answered over 391 joined rows, from 35 open and 23 answered, with no stale declaration.go test ./... -count=1passes withGITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred, and every touched package keeps 100% statement coverage:cmd/audit_edition_tier,cmd/audit_1to1/internal/structs,issues,mergerequests,notifications,projectsandinternal/tools. The two packages whose code changed have every condition evaluated both ways under gobco (notifications48 of 48,projects1386 of 1386) and no lived or uncovered mutant (30 and 681 killed). Inissuesandmergerequestsonly struct tags change, incmd/audit_edition_tierandcmd/audit_1to1/internal/structsonly declaration data, and ininternal/toolsonly tests, so their condition and mutant records are the parent's.make check-action-ids,check-meta-descriptions,check-gateway-chars,check-surface-quality,check-doc-tool-names,check-e2e-static,check-godocs,check-test-subtests,check-test-goroutines,check-test-file-names,check-md-escaping,check-tenancy,check-dead-consts,check-sdk-context,check-closed-serversandaudit-catalog-first, the R-INPUT requiredness, R-PATH and SDK scopes ofcmd/audit_1to1, golangci-lint with the e2e tag on the changed packages, markdownlint, the table formatter and the link check are clean, and the e2e packages compile. The struct audit reports the same 20 missing input fields as the parent and no stale declaration.The tool snapshots, the generated tool reference in English and Spanish, the footprint, the request inventory and the testing reference change with these schemas and are regenerated once at the top of the stack. This layer edits no hand-written file that #1517 changes; the generated artifacts both change are regenerated by whichever of the two lands second.